package org.apache.lucene.ocean;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.StaleReaderException;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.ocean.Deletes.DeleteByQuery;
import org.apache.lucene.ocean.util.Constants;
import org.apache.lucene.ocean.util.SortedListMap;
import org.apache.lucene.ocean.util.Util;
import org.apache.lucene.search.ExtendedFieldCache;
import org.apache.lucene.search.HitCollector;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searchable;
import org.apache.lucene.search.ExtendedFieldCache.LongParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// TODO: implement close, and isClosed
/**
 * The Index class really represents a Lucene segment.  Index manages
 * the IndexSnapshots.  
 */
public abstract class Index {
  final static Logger LOG = LoggerFactory.getLogger(Index.class);
  private final IndexID id;
  protected boolean isClosed = false;
  private final TransactionSystem transactionSystem;
  private boolean isReadOnly = false;
  private boolean isDeleteOnly = false;
  protected List<IndexID> mergedfromIndexIds = new ArrayList<IndexID>();

  public Index(IndexID id, TransactionSystem transactionSystem) {
    this.id = id;
    this.transactionSystem = transactionSystem;
  }

  public static interface IndexSnapshotSearchable extends Searchable {
    public IndexSnapshot getIndexSnapshot();
  }

  public static class IndexSnapshotSearcher extends IndexSearcher implements IndexSnapshotSearchable {
    private IndexSnapshot indexSnapshot;

    public IndexSnapshotSearcher(IndexReader indexReader, IndexSnapshot indexSnapshot) {
      super(indexReader);
      this.indexSnapshot = indexSnapshot;
    }

    public IndexSnapshot getIndexSnapshot() {
      return indexSnapshot;
    }
  }

  protected void addMergedFromIndexIds(Collection<? extends IndexSnapshot> indexSnapshots) {
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      mergedfromIndexIds.add(indexSnapshot.getIndex().getId());
    }
  }

  protected void addMergedFromIndexId(IndexSnapshot indexSnapshot) {
    mergedfromIndexIds.add(indexSnapshot.getIndex().getId());
  }

  public List<IndexID> getMergedFromIndexIds() {
    return mergedfromIndexIds;
  }

  /**
   * Remove indexsnapshots after N descending
   * 
   * @param snapshotMap
   * @throws Exception
   */
  // TODO: check main snapshot
  public void removeOldSnapshots(SortedListMap<Long,? extends IndexSnapshot> snapshotMap) throws Exception {
    ListIterator<? extends Map.Entry<Long,? extends IndexSnapshot>> iterator = snapshotMap.endListIterator();
    int count = 0;
    List<IndexSnapshot> toDelete = new ArrayList<IndexSnapshot>();
    while (iterator.hasPrevious()) {
      Map.Entry<Long,? extends IndexSnapshot> entry = iterator.previous();
      if (count > 5) {
        IndexSnapshot indexSnapshot = entry.getValue();
        Snapshots snapshots = transactionSystem.getSnapshots();
        Snapshot snapshot = snapshots.get(indexSnapshot.getSnapshotId());
        try {
          int refCount = snapshot.refCount();
          if (!snapshots.hasRefs(indexSnapshot.getSnapshotId())) {
            toDelete.add(indexSnapshot);
          }
        } finally {
          snapshot.decRef();
        }
        // indexSnapshot.delete();
      }
      count++;
    }
    for (IndexSnapshot indexSnapshot : toDelete) {
      indexSnapshot.delete();
    }
    /**
     * Long last = snapshotMap.lastKey(); for (Iterator<Long> iterator =
     * snapshotMap.keySet().iterator(); iterator.hasNext();) { Long snapshotId =
     * iterator.next(); if (!transactionSystem.snapshots.contains(snapshotId) &&
     * (last != null && !last.equals(snapshotId))) { iterator.remove(); } }
     */
  }

  public void close() throws IOException {
  }

  public TransactionSystem getSystem() {
    return transactionSystem;
  }
  
  /**
   * Get the index readers
   * @param indexSnapshots
   * @return
   */
  public static IndexReader[] getIndexReaders(List<? extends IndexSnapshot> indexSnapshots) {
    IndexReader[] indexReaders = new IndexReader[indexSnapshots.size()];
    for (int x = 0; x < indexSnapshots.size(); x++) {
      indexReaders[x] = indexSnapshots.get(x).getIndexReader();
    }
    return indexReaders;
  }
  
  /**
   * Get the maximum snapshot id
   * @param indexSnapshots
   * @return
   * @throws Exception
   */
  public static Long getMaxSnapshotId(List<? extends IndexSnapshot> indexSnapshots) throws Exception {
    List<Long> snapshotIdList = new ArrayList<Long>(indexSnapshots.size());
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      snapshotIdList.add(indexSnapshot.getMaxSnapshotId());
    }
    if (snapshotIdList.size() > 0) {
      return Collections.max(snapshotIdList);
    } else {
      return null;
    }
  }
  
  /**
   * Get the maximum document id
   * @param indexSnapshots
   * @return
   * @throws Exception
   */
  public static Long getMaxDocumentId(List<? extends IndexSnapshot> indexSnapshots) throws Exception {
    List<Long> documentIdList = new ArrayList<Long>(indexSnapshots.size());
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      documentIdList.add(indexSnapshot.getMaxDocumentId());
    }
    if (documentIdList.size() > 0) {
      return Collections.max(documentIdList);
    } else {
      return null;
    }
  }

  public void setDeleteOnly(boolean isDeleteOnly) {
    this.isDeleteOnly = isDeleteOnly;
  }

  public boolean isDeleteOnly() {
    return isDeleteOnly;
  }

  public boolean isReadOnly() {
    return isReadOnly;
  }

  public void setReadOnly(boolean isReadOnly) {
    this.isReadOnly = isReadOnly;
  }
 
  /**
   * Called by a Transaction to delete documents from the given IndexReader
   * @param deleteFromReader Whether or not to delete from the reader
   * @param deletes Deletes to apply
   * @param deletedDocs 
   * @param indexReader
   * @return 
   * @throws CorruptIndexException
   * @throws IOException
   * @throws Exception
   */
  protected IndexSnapshotCommitResult applyDeletes(boolean deleteFromReader, Deletes deletes, Collection<Integer> deletedDocs, IndexReader indexReader)
      throws CorruptIndexException, IOException, Exception {
    long[] ids = ExtendedFieldCache.EXT_DEFAULT.getLongs(indexReader, Constants.DOCUMENTID, new LongParser() {
      public long parseLong(String string) {
        return Util.longFromEncoded(string);
      }
    });
    IndexSnapshotCommitResult deletesResult = new IndexSnapshotCommitResult(getId());
    if (deletes.hasDeletes()) {
      if (deletes.hasDocIds()) {
        int docsDeleted = 0;
        long[] docIdsArray = deletes.getDocIds();
        for (long id : docIdsArray) {
          int doc = Util.getDoc(Constants.DOCUMENTID, id, indexReader);
          if (doc >= 0) {
            if (deleteFromReader)
              indexReader.deleteDocument(doc);
            if (deletedDocs != null)
              deletedDocs.add(doc);
            docsDeleted++;
          }
        }
        deletesResult.add(new IndexSnapshotCommitResult.Result(docIdsArray, docsDeleted));
      } else {
        List<Long> docIds = deletesResult.getDocIds();
        if (deletes.hasTerms()) {
          List<Term> terms = deletes.getTerms();
          for (Term term : terms) {
            int docsDeleted = deleteByTerm(deleteFromReader, term, deletedDocs, docIds, ids, indexReader);

            deletesResult.add(new IndexSnapshotCommitResult.Result(term, docsDeleted));
          }
        }
        if (deletes.hasDeleteByQueries()) {
          List<DeleteByQuery> deleteByQueries = deletes.getDeleteByQueries();
          for (DeleteByQuery deleteByQuery : deleteByQueries) {
            int docsDeleted = deleteByQuery(deleteFromReader, deleteByQuery, deletedDocs, ids, docIds, indexReader);
            deletesResult.add(new IndexSnapshotCommitResult.Result(deleteByQuery, docsDeleted));
          }
        }
      }
    }
    return deletesResult;
  }
  
  /**
   * Delete a 
   * @param deleteFromReader Whether to delete from the reader
   * @param term Term to delete
   * @param deletedDocs If passed in collection to add the deleted doc numbers to
   * @param docIds documentids of the deleted documents
   * @param ids Ids of the documents to delete
   * @param indexReader
   * @return
   * @throws IOException
   */
  protected int deleteByTerm(boolean deleteFromReader, Term term, Collection<Integer> deletedDocs, List<Long> docIds, long[] ids,
      IndexReader indexReader) throws IOException {
    TermDocs docs = indexReader.termDocs(term);
    if (docs == null)
      return 0;
    int n = 0;
    try {
      while (docs.next()) {
        int doc = docs.doc();
        if (deletedDocs != null)
          deletedDocs.add(doc);
        Long docId = ids[doc];
        docIds.add(docId);
        if (deleteFromReader)
          indexReader.deleteDocument(doc);
        n++;
      }
    } finally {
      docs.close();
    }
    return n;
  }

  protected int deleteByQuery(final boolean deleteFromReader, DeleteByQuery deleteByQuery, final Collection<Integer> deletedDocs,
      final long[] ids, final List<Long> deletedIds, final IndexReader indexReader) throws IOException {
    Query query = deleteByQuery.getQuery();
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);
    final int[] numDeleted = new int[1];
    indexSearcher.search(query, new HitCollector() {
      public void collect(int doc, float score) {
        try {
          if (deleteFromReader)
            indexReader.deleteDocument(doc);
          if (deletedDocs != null)
            deletedDocs.add(doc);
          Long docId = ids[doc];
          deletedIds.add(docId);
          numDeleted[0]++;
        } catch (StaleReaderException staleReaderException) {
          throw new RuntimeException(staleReaderException);
        } catch (IOException ioException) {
          throw new RuntimeException(ioException);
        }
      }
    });
    return numDeleted[0];
  }

  public static class IndexException extends Exception {
    public IndexException(String message) {
      super(message);
    }

    public IndexException(String message, Exception exception) {
      super(message, exception);
    }
  }

  public static class IndexNeverCompletedCopyException extends IndexException {
    public IndexNeverCompletedCopyException(String message) {
      super(message);
    }
  }

  public abstract boolean rollback(Long snapshotId) throws Exception;

  public abstract IndexSnapshot getIndexSnapshot(Long snapshotID);

  public abstract IndexSnapshot getLatestIndexSnapshot();

  public abstract class IndexSnapshot {
    protected final Long snapshotId;

    public IndexSnapshot(Long snapshotId) {
      this.snapshotId = snapshotId;
    }

    public IndexSnapshotSearchable getSearcher() {
      return new IndexSnapshotSearcher(getIndexReader(), this);
    }

    public abstract void delete() throws Exception;

    public abstract Long getMinDocumentId() throws IOException;

    public abstract Long getMinSnapshotId() throws IOException;

    public abstract Long getMaxSnapshotId() throws IOException;

    public abstract Long getMaxDocumentId() throws IOException;

    public abstract int deletedDoc();

    public abstract int maxDoc();

    public Long getSnapshotId() {
      return snapshotId;
    }

    public Index getIndex() {
      return Index.this;
    }

    /**
     * Iterates terms until next field is reached, returns text.
     * When the tag index is implemented this should be a straight call to
     * a max method.
     * 
     * @param field
     * @return
     * @throws Exception
     */
    public String getMax(String field) throws IOException {
      IndexReader indexReader = getIndexReader();
      TermEnum termEnum = indexReader.terms(new Term(field, ""));
      try {
        String text = null;
        do {
          Term term = termEnum.term();
          if (term == null || term.field() != field)
            break;
          text = term.text();
        } while (termEnum.next());
        return text;
      } finally {
        termEnum.close();
      }
    }

    public String getMin(String field) throws IOException {
      IndexReader indexReader = getIndexReader();
      TermEnum termEnum = indexReader.terms(new Term(field, ""));
      try {
        do {
          Term term = termEnum.term();
          if (term == null || term.field() != field)
            break;
          return term.text();
        } while (termEnum.next());
        return null;
      } finally {
        termEnum.close();
      }
    }

    public abstract IndexReader getIndexReader();
  }

  public static class MergedDocMap {
    private Map<IndexSnapshot,int[]> oldMap; // maps old doc to new doc
    private RI[] merged; // maps new doc to old doc and reader

    public MergedDocMap(List<? extends IndexSnapshot> indexSnapshots) {
      int newMaxDoc = 0;
      for (IndexSnapshot indexSnapshot : indexSnapshots) {
        newMaxDoc += indexSnapshot.getIndexReader().numDocs();
      }
      oldMap = new HashMap<IndexSnapshot,int[]>(indexSnapshots.size());
      RI[] merged = new RI[newMaxDoc];
      int pos = 0;
      for (IndexSnapshot indexSnapshot : indexSnapshots) {
        IndexReader indexReader = indexSnapshot.getIndexReader();
        int maxDoc = indexReader.maxDoc();
        int[] old = new int[maxDoc];
        for (int x = 0; x < maxDoc; x++) {
          if (indexReader.hasDeletions() && indexReader.isDeleted(x)) {
            merged[pos] = null;
            old[x] = -1;
          } else {
            merged[pos] = new RI(x, indexSnapshot);
            old[x] = pos;
            pos++;
          }
        }
        oldMap.put(indexSnapshot, old);
      }
    }

    public static class RI {
      public int doc;
      public IndexSnapshot oldIndexSnapshot;

      public RI(int doc, IndexSnapshot oldIndexSnapshot) {
        this.doc = doc;
        this.oldIndexSnapshot = oldIndexSnapshot;
      }
    }

    public Map<IndexSnapshot,int[]> getOldMap() {
      return oldMap;
    }

    public RI[] getMerged() {
      return merged;
    }
  }

  public boolean isClosed() {
    return isClosed;
  }

  public IndexID getId() {
    return id;
  }

  public abstract void commitNothing(Transaction transaction) throws IndexException, InterruptedException, IOException, Exception;

  public abstract IndexSnapshotCommitResult commitDeletes(Deletes deletes, Transaction transaction) throws Exception, IndexException,
      InterruptedException, IOException;
}
